<html lang="en-US">

<head>
    
<title>fmt 标准库 --- Print* 是怎么样输出的？ - 花落雨忧</title>

<meta property="og:title" content="fmt 标准库 --- Print* 是怎么样输出的？ - 花落雨忧">



    



    
    <meta property="description" content="package main import ( &amp;quot;fmt&amp;quot; ) func main() { fmt.Println(&amp;quot;Hello World!&amp;quot;) } 标准开场见多了，那内部标准库又是怎么输出这段英文的呢？今天一起来围观下源码吧 🤭
[&amp;hellip;] func Print(a ...interface{}) (n int, err error) { &amp;hellip;">
    <meta property="og:description" content="package main import ( &amp;quot;fmt&amp;quot; ) func main() { fmt.Println(&amp;quot;Hello World!&amp;quot;) } 标准开场见多了，那内部标准库又是怎么输出这段英文的呢？今天一起来围观下源码吧 🤭
[&amp;hellip;] func Print(a ...interface{}) (n int, err error) { &amp;hellip;">
    






<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1">

<link rel="shortcut icon" href="https://www.lican.site/logo/logo.png" type="image/x-icon" />



<link rel="stylesheet" href="/css/style.min.css" />

<link rel="stylesheet" href="/css/reset.min.css" />




<script src="https://www.lican.site/js/highlight.min.js"></script>

<script>
hljs.configure({ ignoreUnescapedHTML: true })
hljs.highlightAll();
</script>


<script src="https://www.lican.site/js/jquery.min.js"></script>




<link href="https://www.lican.site/css/hugo-code.min.css" rel="stylesheet" />



    <style>
        .post-content img {
            max-width: 400px;
        }
    </style>
</head>

<body id="period" class="home blog">
    <a class="skip-content" href="#main">Press "Enter" to skip to content</a>
    <div id="overflow-container" class="overflow-container">
        <header class="site-header" id="site-header" role="banner">
    <div class="max-width">
        <div id="title-container" class="title-container">
            <div id="site-title" class="site-title"><a href="/">花落雨忧</a></div>
            <p class="tagline">思所及 力所行 方为真.</p>
        </div>
        
        <div id="menu-primary-container" class="menu-primary-container">
            <div class="icon-container">
            </div>
            <div id="menu-primary" class="menu-container menu-primary" role="navigation">
                <nav class="menu">
                    <ul id="menu-primary-items" class="menu-primary-items">
                        
                        
                        <li id="menu-item-0"
                            class="menu-item menu-item-type-custom menu-item-object-custom ">
                            <a href="/posts" aria-current="page" tabindex="0">首页</a></li>
                        
                        <li id="menu-item-1"
                            class="menu-item menu-item-type-custom menu-item-object-custom ">
                            <a href="/tech/" aria-current="page" tabindex="1">技术文档</a></li>
                        
                        <li id="menu-item-2"
                            class="menu-item menu-item-type-custom menu-item-object-custom ">
                            <a href="/article/" aria-current="page" tabindex="2">文章</a></li>
                        
                        <li id="menu-item-3"
                            class="menu-item menu-item-type-custom menu-item-object-custom ">
                            <a href="/project/" aria-current="page" tabindex="3">项目</a></li>
                        
                        <li id="menu-item-4"
                            class="menu-item menu-item-type-custom menu-item-object-custom ">
                            <a href="/about/" aria-current="page" tabindex="4">关于</a></li>
                        
                    </ul>
                </nav>
            </div>
        </div>
    </div>
</header>
        <div id="primary-container" class="primary-container">
            <div class="max-width">
                <section id="main" class="main" role="main">
                    <div id="loop-container" class="loop-container">
                        <div
                            class="post type-post status-publish format-standard hentry entry">
                            <article>
                                <div class="post-container">
                                    <div class="post-header">
                                        <h2 class="post-title">
                                            <a href="/posts/go/pkg/2018-12-04-fmt/">fmt 标准库 --- Print* 是怎么样输出的？</a>
                                        </h2>
                                        
                                        <div class="post-byline">Published on
                                            <a class="date" href="javascript:;">2018/12/04</a>
                                            
                                            
                                            
                                            By 
                                            <a class="author" href="javascript:;">煎鱼</a></div>
                                            
                                        
                                    </div>
                                    <div class="post-content">
                                        <h2 id="前言">前言</h2>
<pre><code>package main

import (
	&quot;fmt&quot;
)

func main() {
	fmt.Println(&quot;Hello World!&quot;)
}
</code></pre><p>标准开场见多了，那内部标准库又是怎么输出这段英文的呢？今天一起来围观下源码吧 🤭</p>
<h2 id="原型">原型</h2>
<pre><code>func Print(a ...interface{}) (n int, err error) {
	return Fprint(os.Stdout, a...)
}

func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}
</code></pre><ul>
<li>Print：使用默认格式说明符打印格式并写入标准输出。当两者都不是字符串时，在操作数之间添加空格</li>
<li>Println：同上，不同的地方是始终在操作数之间添加空格，并附加换行符</li>
<li>Printf：根据格式说明符进行格式化并写入标准输出</li>
</ul>
<p>以上三类就是最常见的格式化 I/O 的方法，我们将基于此去进行拆解描述</p>
<h2 id="执行流程">执行流程</h2>
<h3 id="案例一print">案例一：Print</h3>
<p>在这里我们使用 <code>Print</code> 方法做一个分析，便于后面的加深理解 😄</p>
<pre><code>func Print(a ...interface{}) (n int, err error) {
	return Fprint(os.Stdout, a...)
}
</code></pre><p><code>Print</code> 使用默认格式说明符打印格式并写入标准输出。另外当两者都为非空字符串时将插入一个空格</p>
<h4 id="原型-1">原型</h4>
<pre><code>func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrint(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}
</code></pre><p>该函数一共有两个形参：</p>
<ul>
<li>w：输出流，只要实现 io.Writer 就可以（抽象）为流的写入</li>
<li>a：任意类型的多个值</li>
</ul>
<h4 id="分析主干流程">分析主干流程</h4>
<p>1、 p := newPrinter(): 申请一个临时对象池（sync.Pool）</p>
<pre><code>var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}

func newPrinter() *pp {
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.fmt.init(&amp;p.buf)
	return p
}
</code></pre><ul>
<li>ppFree.Get()：基于 sync.Pool 实现 *pp 的临时对象池，每次获取一定会返回一个新的 pp 对象用于接下来的处理</li>
<li>*pp.panicking：用于解决无限递归的 panic、recover 问题，会根据该参数在 catchPanic 及时掐断</li>
<li>*pp.erroring：用于表示正在处理错误无效的 verb 标识符，主要作用是防止调用 handleMethods 方法</li>
<li>*pp.fmt.init(&amp;p.buf)：初始化 fmt 配置，会设置 buf 并且清空 fmtFlags 标志位</li>
</ul>
<p>2、 p.doPrint(a): 执行约定的格式化动作（参数间增加一个空格、最后一个参数增加换行符）</p>
<pre><code>func (p *pp) doPrint(a []interface{}) {
	prevString := false
	for argNum, arg := range a {
	    true &amp;&amp; false
		isString := arg != nil &amp;&amp; reflect.TypeOf(arg).Kind() == reflect.String
		// Add a space between two non-string arguments.
		if argNum &gt; 0 &amp;&amp; !isString &amp;&amp; !prevString {
			p.buf.WriteByte(' ')
		}
		p.printArg(arg, 'v')
		prevString = isString
	}
}
</code></pre><p>可以看到底层通过判断该入参，<strong>同时</strong>满足以下条件就会添加分隔符（空格）：</p>
<ul>
<li>当前入参为多个参数（例如：Slice）</li>
<li>当前入参不为 nil 且不为字符串（通过反射确定）</li>
<li>当前入参不为首项或上一个入参不为字符串</li>
</ul>
<p>而在 <code>Print</code> 方法中，不需要指定格式符。实际上在该方法内直接指定为 <code>v</code>。也就是默认格式的值</p>
<pre><code>p.printArg(arg, 'v')
</code></pre><ol start="3">
<li>
<p>w.Write(p.buf): 写入标准输出（io.Writer）</p>
</li>
<li>
<p>*pp.free(): 释放已缓存的内容。在使用完临时对象后，会将 buf、arg、value 清空再重新存放到 ppFree 中。以便于后面再取出重用（利用 sync.Pool 的临时对象特性）</p>
</li>
</ol>
<h3 id="案例二printf">案例二：Printf</h3>
<h4 id="标识符">标识符</h4>
<h5 id="verbs">Verbs</h5>
<pre><code>%v	the value in a default format
	when printing structs, the plus flag (%+v) adds field names
%#v	a Go-syntax representation of the value
%T	a Go-syntax representation of the type of the value
%%	a literal percent sign; consumes no value
%t	the word true or false
</code></pre><h5 id="flags">Flags</h5>
<pre><code>+	always print a sign for numeric values;
	guarantee ASCII-only output for %q (%+q)
-	pad with spaces on the right rather than the left (left-justify the field)
#	alternate format: add leading 0 for octal (%#o), 0x for hex (%#x);
	0X for hex (%#X); suppress 0x for %p (%#p);
	for %q, print a raw (backquoted) string if strconv.CanBackquote
	returns true;
	always print a decimal point for %e, %E, %f, %F, %g and %G;
	do not remove trailing zeros for %g and %G;
	write e.g. U+0078 'x' if the character is printable for %U (%#U).
' '	(space) leave a space for elided sign in numbers (% d);
	put spaces between bytes printing strings or slices in hex (% x, % X)
0	pad with leading zeros rather than spaces;
	for numbers, this moves the padding after the sign
</code></pre><p>详细建议参见 <a href="https://golang.org/pkg/fmt/#hdr-Printing">Godoc</a></p>
<h4 id="原型-2">原型</h4>
<pre><code>func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}
</code></pre><p>与 Print 相比，最大的不同就是 doPrintf 方法了。在这里我们来详细看看其代码，如下：</p>
<pre><code>func (p *pp) doPrintf(format string, a []interface{}) {
	end := len(format)
	argNum := 0         // we process one argument per non-trivial format
	afterIndex := false // previous item in format was an index like [3].
	p.reordered = false
formatLoop:
	for i := 0; i &lt; end; {
		p.goodArgNum = true
		lasti := i
		for i &lt; end &amp;&amp; format[i] != '%' {
			i++
		}
		if i &gt; lasti {
			p.buf.WriteString(format[lasti:i])
		}
		if i &gt;= end {
			// done processing format string
			break
		}

		// Process one verb
		i++

		// Do we have flags?
		p.fmt.clearflags()
	simpleFormat:
		for ; i &lt; end; i++ {
			c := format[i]
			switch c {
			case '#':   //'#'、'0'、'+'、'-'、' '
				...
			default:
				if 'a' &lt;= c &amp;&amp; c &lt;= 'z' &amp;&amp; argNum &lt; len(a) {
					...
					p.printArg(a[argNum], rune(c))
					argNum++
					i++
					continue formatLoop
				}
				
				break simpleFormat
			}
		}

		// Do we have an explicit argument index?
		argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))

		// Do we have width?
		if i &lt; end &amp;&amp; format[i] == '*' {
			...
		}

		// Do we have precision?
		if i+1 &lt; end &amp;&amp; format[i] == '.' {
			...
		}

		if !afterIndex {
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
		}

		if i &gt;= end {
			p.buf.WriteString(noVerbString)
			break
		}

		...

		switch {
		case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
			p.buf.WriteByte('%')
		case !p.goodArgNum:
			p.badArgNum(verb)
		case argNum &gt;= len(a): // No argument left over to print for the current verb.
			p.missingArg(verb)
		case verb == 'v':
			...
			fallthrough
		default:
			p.printArg(a[argNum], verb)
			argNum++
		}
	}

	if !p.reordered &amp;&amp; argNum &lt; len(a) {
		...
	}
}
</code></pre><h4 id="分析主干流程-1">分析主干流程</h4>
<ol>
<li>写入 % 之前的字符内容</li>
<li>如果所有标志位处理完毕（到达字符尾部），则跳出处理逻辑</li>
<li>（往后移）跳过 % ，开始处理其他 verb 标志位</li>
<li>清空（重新初始化） fmt 配置</li>
<li>处理一些基础的 verb 标识符（simpleFormat）。如：'#'、&lsquo;0&rsquo;、&lsquo;+&rsquo;、'-'、&rsquo; ' 以及<strong>简单的 verbs 标识符（不包含精度、宽度和参数索引）。需要注意的是，若当前字符为简单 verb 标识符。则直接进行处理。完成后会直接后移到下一个字符</strong>。其余标志位则变更 fmt 配置项，便于后续处理</li>
<li>处理参数索引（argument index）</li>
<li>处理参数宽度（width）</li>
<li>处理参数精度（precision）</li>
<li>% 之后若不存在 verbs 标识符则返回 <code>noVerbString</code>。值为 %!(NOVERB)</li>
<li>处理特殊 verbs 标识符（如：'%%'、'%#v&rsquo;、'%+v&rsquo;）、错误情况（如：参数索引指定错误、参数集个数与 verbs 标识符数量不匹配）或进行格式化参数集</li>
<li>常规流程处理完毕</li>
</ol>
<p>在特殊情况下，若提供的参数集比 verb 标识符多。fmt 将会贪婪检查下去，将多出的参数集以特定的格式输出，如下：</p>
<pre><code>fmt.Printf(&quot;%d&quot;, 1, 2, 3)
// 1%!(EXTRA int=2, int=3)
</code></pre><ul>
<li>约定前缀额外标志：%!(EXTRA</li>
<li>当前参数的类型</li>
<li>约定格式符：=</li>
<li>当前参数的值（默认以 %v 格式化）</li>
<li>约定格式符：)</li>
</ul>
<p>值得注意的是，当指定了参数索引或实际处理的参数小于入参的参数集时，就不会进行贪婪匹配来展示</p>
<h3 id="案例三println">案例三：Println</h3>
<h4 id="原型-3">原型</h4>
<pre><code>func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintln(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}
</code></pre><p>在这个方法中，最大的区别就是 doPrintln，我们一起来看看，如下：</p>
<pre><code>func (p *pp) doPrintln(a []interface{}) {
	for argNum, arg := range a {
		if argNum &gt; 0 {
			p.buf.WriteByte(' ')
		}
		p.printArg(arg, 'v')
	}
	p.buf.WriteByte('\n')
}
</code></pre><h4 id="分析主干流程-2">分析主干流程</h4>
<ul>
<li>循环入参的参数集，并以空格分隔</li>
<li>格式化当前参数，默认以 <code>%v</code> 对参数进行格式化</li>
<li>在结尾添加 <code>\n</code> 字符</li>
</ul>
<h2 id="如何格式化参数">如何格式化参数</h2>
<p>在上例的执行流程分析中，可以看到格式化参数这一步是在 <code>p.printArg(arg, verb)</code> 执行的，我们一起来看看它都做了些什么？</p>
<pre><code>func (p *pp) printArg(arg interface{}, verb rune) {
	p.arg = arg
	p.value = reflect.Value{}

	if arg == nil {
		switch verb {
		case 'T', 'v':
			p.fmt.padString(nilAngleString)
		default:
			p.badVerb(verb)
		}
		return
	}

	switch verb {
	case 'T':
		p.fmt.fmt_s(reflect.TypeOf(arg).String())
		return
	case 'p':
		p.fmtPointer(reflect.ValueOf(arg), 'p')
		return
	}

	// Some types can be done without reflection.
	switch f := arg.(type) {
	case bool:
		p.fmtBool(f, verb)
	case float32:
		p.fmtFloat(float64(f), 32, verb)
	...
	case reflect.Value:
		if f.IsValid() &amp;&amp; f.CanInterface() {
			p.arg = f.Interface()
			if p.handleMethods(verb) {
				return
			}
		}
		p.printValue(f, verb, 0)
	default:
		if !p.handleMethods(verb) {
			p.printValue(reflect.ValueOf(f), verb, 0)
		}
	}
}
</code></pre><p>在小节代码中可以看见，fmt 本身对不同的类型做了不同的处理。这样子就避免了通过反射确定。相对的提高了性能</p>
<p>其中有两个特殊的方法，分别是 <code>handleMethods</code> 和 <code>badVerb</code>，接下来分别来看看他们的作用是什么</p>
<p>1、badVerb</p>
<p>它主要用于格式化并处理错误的行为。我们可以一起来看看，代码如下：</p>
<pre><code>func (p *pp) badVerb(verb rune) {
	p.erroring = true
	p.buf.WriteString(percentBangString)
	p.buf.WriteRune(verb)
	p.buf.WriteByte('(')
	switch {
	case p.arg != nil:
		p.buf.WriteString(reflect.TypeOf(p.arg).String())
		p.buf.WriteByte('=')
		p.printArg(p.arg, 'v')
	...
	default:
		p.buf.WriteString(nilAngleString)
	}
	p.buf.WriteByte(')')
	p.erroring = false
}
</code></pre><p>在处理错误格式化时，我们可以对比以下例子：</p>
<pre><code>fmt.Printf(&quot;%s&quot;, []int64{1, 2, 3})
// [%!s(int64=1) %!s(int64=2) %!s(int64=3)]%
</code></pre><p>在 badVerb 中可以看到错误字符串的处理主要分为以下部分：</p>
<ul>
<li>约定前缀错误标志：%!</li>
<li>当前的格式化操作符</li>
<li>约定格式符：(</li>
<li>当前参数的类型</li>
<li>约定格式符：=</li>
<li>当前参数的值（默认以 %v 格式化）</li>
<li>约定格式符：)</li>
</ul>
<p>2、handleMethods</p>
<pre><code>func (p *pp) handleMethods(verb rune) (handled bool) {
	if p.erroring {
		return
	}
	// Is it a Formatter?
	if formatter, ok := p.arg.(Formatter); ok {
		handled = true
		defer p.catchPanic(p.arg, verb)
		formatter.Format(p, verb)
		return
	}

	// If we're doing Go syntax and the argument knows how to supply it, take care of it now.
	...
	
	return false
}
</code></pre><p>这个方法比较特殊，一般在自定义结构体和未知情况下进行调用。主要流程是：</p>
<ul>
<li>若当前参数为错误 verb 标识符，则直接返回</li>
<li>判断是否实现了 Formatter</li>
<li>实现，则利用自定义 Formatter 格式化参数</li>
<li>未实现，则最大程度的利用 Go syntax 默认规则去格式化参数</li>
</ul>
<h2 id="拓展">拓展</h2>
<p>在 fmt 标准库中可以通过自定义结构体来实现方法的自定义，大致如下几种</p>
<h3 id="fmtstate">fmt.State</h3>
<pre><code>type State interface {
	Write(b []byte) (n int, err error)

	Width() (wid int, ok bool)

	Precision() (prec int, ok bool)

	Flag(c int) bool
}
</code></pre><p>State 用于获取标志位的状态值，涉及如下：</p>
<ul>
<li>Write：将格式化完毕的字符写入缓冲区中，等待下一步处理</li>
<li>Width：返回宽度信息和是否被设置</li>
<li>Precision：返回精度信息和是否被设置</li>
<li>Flag：返回特殊标志符（'#'、&lsquo;0&rsquo;、&lsquo;+&rsquo;、'-'、&rsquo; &lsquo;）是否被设置</li>
</ul>
<h3 id="fmtformatter">fmt.Formatter</h3>
<pre><code>type Formatter interface {
	Format(f State, c rune)
}
</code></pre><p>Formatter 用于实现<strong>自定义格式化方法</strong>。可通过在自定义结构体中实现 Format 方法来实现这个目的</p>
<p>另外，可以通过 f 获取到当前标识符的宽度、精度等状态值。c 为 verb 标识符，可以得到其动作是什么</p>
<h3 id="fmtstringer">fmt.Stringer</h3>
<pre><code>type Stringer interface {
	String() string
}
</code></pre><p>当该对象为 String、Array、Slice 等类型时，将会调用 <code>String()</code> 方法对类字符串进行格式化</p>
<h3 id="fmtgostringer">fmt.GoStringer</h3>
<pre><code>type GoStringer interface {
	GoString() string
}
</code></pre><p>当格式化特定 verb 标识符（%v）时，将调用 <code>GoString()</code> 方法对其进行格式化</p>
<h2 id="总结">总结</h2>
<p>通过本文对 fmt 标准库的分析，可以发现它有以下特点：</p>
<ul>
<li>在拓展性方面，可以自定义格式化方法等</li>
<li>在完整度方面，尽可能的贪婪匹配，输出参数集</li>
<li>在性能方面，每种不同的参数类型，都实现了不同的格式化处理操作</li>
<li>在性能方面，尽可能的最短匹配，格式化参数集</li>
</ul>
<p>总的来说，fmt 标准库有许多值得推敲的细节，希望你能够在本文学到 😄</p>


                                        
                                        
                                        
                                        <div class="rp4wp-related-posts">
                                            <h3>相关文章</h3>
                                            <ul>
                                                
                                                <li>
                                                    <div class="rp4wp-related-post-content">
                                                        <a
                                                            href="https://www.lican.site/posts/go/pkg/2018-09-28-log/">log 标准库</a>
                                                    </div>
                                                </li>
                                                
                                                <li>
                                                    <div class="rp4wp-related-post-content">
                                                        <a
                                                            href="https://www.lican.site/posts/go/talk/2018-11-25-gomock/">使用 Gomock 进行单元测试</a>
                                                    </div>
                                                </li>
                                                
                                                <li>
                                                    <div class="rp4wp-related-post-content">
                                                        <a
                                                            href="https://www.lican.site/posts/go/grpc/2018-10-20-zipkin/">「连载十」分布式链路追踪 gRPC &#43; Opentracing &#43; Zipkin</a>
                                                    </div>
                                                </li>
                                                
                                                <li>
                                                    <div class="rp4wp-related-post-content">
                                                        <a
                                                            href="https://www.lican.site/posts/go/grpc/2018-10-16-deadlines/">「连载九」gRPC Deadlines</a>
                                                    </div>
                                                </li>
                                                
                                                <li>
                                                    <div class="rp4wp-related-post-content">
                                                        <a
                                                            href="https://www.lican.site/posts/go/grpc/2018-10-14-per-rpc-credentials/">「连载八」对 RPC 方法做自定义认证</a>
                                                    </div>
                                                </li>
                                                
                                            </ul>
                                        </div>
                                        
                                        
                                    </div>

                                    
                                    
                                    

                                    
                                    <div class="post-meta">
                                        
                                        
                                        <div class="post-tags">
                                            <ul>
                                            
                                            <li>
                                                <a href="/tags/go" title="View all posts tagged match">go</a>
                                            </li>
                                            
                                            <li>
                                                <a href="/tags/%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90" title="View all posts tagged match">源码分析</a>
                                            </li>
                                            
                                            </ul>
                                        </div>
                                        
                                        
                                        <nav class="further-reading">
                                            
                                            <div class="previous">
                                                <span>&lt;&lt; Prev</span>
                                                <a href="https://www.lican.site/posts/go/talk/2018-11-25-gomock/"
                                                    rel="prev">使用 Gomock 进行单元测试</a> </div>
                                            
                                            
                                            <div class="next">
                                                <span>Next >></span>
                                                <a href="https://www.lican.site/posts/go/slice/2018-12-11-slice/">深入理解 Go Slice</a> 
                                            </div>
                                            
                                        </nav>
                                    </div>
                                    
                                    

                                    
                                    
                                    

                                </div>
                            </article>
                        </div>
                    </div>
                </section>
                <aside class="sidebar sidebar-primary" id="sidebar-primary" role="complementary">
    <h1 class="screen-reader-text">Sidebar</h1>
    

    
    
    <section id="text-2" class="widget widget_text">
        <div class="textwidget">
            
            <div id="profile">
                <div id="profile_picture"><img src="https://www.lican.site/logo/logo.png"></div>
                <div id="profile_intro">
                    <p><span class="name">Lican</span></p>
                    <p class="intro">全栈开发者，爱好造轮子。</p>
                </div>
            </div>
            
            <p>
                <script type="text/javascript">
                    (function ($) {
                        $(document).ready(function () {
                            var menuPrimaryContainer = $('#menu-primary-container');
                            var profile = $('#text-2');
                            $('#toggle-navigation').click(function () {
                                if (menuPrimaryContainer.hasClass('open')) {
                                    profile.removeClass('open');
                                } else {
                                    profile.addClass('open');
                                }
                            });
                        });
                    })(jQuery);
                </script>
            </p>
        </div>
    </section>
    
    
    
    
    
    <section id="text-5" class="widget widget_text">
        <h2 class="widget-title">开源项目</h2>
        <div class="textwidget">
            <div id="projects" style="line-height: 22px;">
                
                <a href="https://github.com/idoubi/gonews"
                    target="_blank">gonews</a>: &nbsp;Daily news for golang<br>
                
                <a href="https://github.com/idoubi/sql2struct"
                    target="_blank">sql2struct</a>: &nbsp;Generate go struct according to SQL<br>
                
                <a href="https://github.com/idoubi/goz"
                    target="_blank">goz</a>: &nbsp;Request library used in golang<br>
                
        </div>
    </section>
    
    

    
    
    
    
    

    
    
    

    
    
    

    
    
    
    
</aside>
            </div>
        </div>

        <footer id="site-footer" class="site-footer" role="contentinfo">
    <div class="max-width">
    </div>
    <div class="footer">
        <div id="footercontent">
            © lican.vip All rights reserved<br/>
            Built with Hugo Theme <a href="https://github.com/idoubi/hugo-theme-period" target="_blank">Period</a>
        </div>
    </div>
</footer>

<script>
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "https://hm.baidu.com/hm.js?e8351b6d4626d5881d439ea1f6184baa";
      var s = document.getElementsByTagName("script")[0]; 
      s.parentNode.insertBefore(hm, s);
    })();
</script>
    
    
    </div>
    
</body>

</html>